大家好,延續昨天的主題,我們希望可以對父層元件body使用新增/移除eventListeners來綁定監聽事件,並且需要一個方法,可以在切換頁面後移除曾經註冊監聽的事件,那就廢話不多說,馬上開始今天的主題吧。
首先在src目錄下建立一個utils資料夾,專門存放共用程序模組。接著建立eventListener.js,作為處理SPA裡監聽事件的共用模組:
接著我們在eventListener.js開頭,宣告常數eventHandlers,並賦予物件型別輸出為模組(之後Router會用到)。這是用來註冊每個Component內的監聽事件處理器:
src/utils/eventListerer.js
export const eventHandlers = {}
這裡說明一下註冊的方式,我們希望eventHandlers可以紀錄當前元件body的所有監聽事件。紀錄的方式為元件在mount之後,從Component內找出要新增的監聽事件類型與事件處理回呼函式(event handler),註冊到這個eventHandlers物件內,同時對body新增監聽事件(addEventListener)。之後切換到別的頁面,會把前次在eventHandlers物件所註冊的監聽事件做取消,同時移除監聽事件(removeEventListener),之後重複循環下去。如此在不同頁面綁定的監聽事件,會隨著切換頁面自動移除,不用擔心重複因新增監聽事件而影響效能。
承上,我們需要建立一個新增監聽事件函式並且匯出,作用是可以將監聽的事件註冊到eventHandlers物件內,同時能夠對body新增監聽事件:
src/utils/eventListerer.js
//對body增加監聽事件
export const addListener = (type, handler, capture = false) => {
//判斷若沒該事件類型,則新增事件屬性並賦予陣列初始值
if (!(type in eventHandlers)) {
eventHandlers[type] = []
}
//註冊handlers
eventHandlers[type].push({
handler: handler,
capture: capture,
})
//對body新增監聽
document.querySelector('body').addEventListener(type, handler, capture)
}
這裡可以看到addListener函式有三個參數可以設置,跟addEventListener裡的參數是一樣的(詳細用法可參閱MDN):
在這裡可以看到eventHandlers註冊的資料格式,屬性為事件類型,值的部份為陣列,裡面每個元素為物件,包含handler與capture:
type:[{
handler: handler,
capture: capture,
}]
接著我們需要再建立一個移除監聽事件函式並匯出,作用是可以把上一次在eventHandlers內註冊的監聽事件做取消,同時移除body的監聽事件:
src/utils/eventListerer.js
//移除body所有監聽事件
export const removeAllListeners = () => {
if (Object.keys(eventHandlers).length) {
//逐個對eventHandlers內的事件類型處理handler
Object.keys(eventHandlers).forEach((type) => {
//逐個對事件類型註冊的handler做處理
eventHandlers[type].forEach(({ handler, capture }) =>
//移除監聽
document
.querySelector('body')
.removeEventListener(type, handler, capture)
)
//將eventHandlers裡的事件回復成初始值
eventHandlers[type] = []
})
}
}
為了對物件內所有事件類型做處理,希望可以輸出陣列搭配forEach做執行,所以用Object.keys()方法,參閱MDN的說明如下:
Object.keys() 方法會回傳一個由指定物件所有可列舉之屬性組成的陣列,該陣列中的的排列順序與使用 for...in 進行迭代的順序相同(兩者的差異在於 for-in 迴圈還會迭代出物件自其原型鏈所繼承來的可列舉屬性)。
我們可以看到forEach會逐個移除曾註冊過的監聽事件,handler是前次新增的,所以不會像昨天一樣找不到同一個。
監聽模組建立好了,接下來我們看看如何在Component內註冊監聽事件。首先在Post內新增一個listener的屬性:
src/pages/Post.js
export const Post = {
listener: {
click: (e) => {
if (e.target.id === 'button') {
console.log('post button clicked')
}
},
},
//...
listener物件專門用來新增元件內的監聽事件。新增監聽事件可以在內部新增事件類型為屬性,值的部份賦予處理監聽事件的handler。如同昨天button的例子,因為是對body綁定監聽事件,這邊使用event.target來判斷冒泡階段的目標。
以上設置好監聽事件註冊模組,也在元件內增加了監聽事件,最後一步就是在Router裡來處理這些行為:
src/routes/Router.js
//引入監聽事件處理模組
import {
eventHandlers,
addListener,
removeAllListeners,
} from '../utils/eventListerer'
export const Router = () => {
//...
// 4.元件render後呼叫
'mount' in component ? component.mount() : null
// 5.處理監聽事件
// 取消全部監聽事件
removeAllListeners()
// 註冊元件內監聽事件
'listener' in component
? Object.keys(component.listener).forEach((type) =>
addListener(type, component.listener[type])
)
: null
// 查看handlers
console.log('eventHandlers:', eventHandlers)
}
我們開頭把監聽事件處理模組所有方法進行匯入,在mount後新增第五步處理監聽事件。首先用removeAllListeners移除之前所有註冊的監聽事件,然後使用addListener對元件內綁定的監聽事件,依照事件類型依序做註冊與新增。另外這裡運到三元運算子,判斷listener屬性是否存在該元件內,防止沒有屬性時瀏覽器報錯。
最後為了確認eventHandlers註冊的內容,使用了console.log輸出來查看,馬上來看看成果會是怎麼樣:
在切換頁面時,可以清楚看到註冊監聽事件模組的eventHandlers資料格式(也就是之前在addListener註冊的資料格式),這裡不用事件目標當作屬性,而是使用監聽事件類型,值的部份為陣列格式,物件元素內包含handler與capture:
然後我們再試試對Post頁面裡的button進行點擊:
一切正常運作,可以看到切換頁面時,綁定body的新增監聽事件不會像之前一樣越來越多,因為監聽模組與Router都處理好了。
自己在使用React時,可以運用Inline events對目標綁定監聽事件,但自己用原生javascript實做SPA的過程中,發現並沒有這麼簡單,參考網路上的方法後,考量有效管理代碼與不影響效能,做出這套管理監聽事件的方法。但因為是對父層元件body監聽,運用事件捕捉與冒泡來判斷目標,所以也需要對目標元素新增屬性標籤(attribute)來判斷目標本身,不像是React直接綁定在元素比較直覺。
參考資料: